/**
 * Project: d20Engine
 * Created: Aug 13, 2006 by bebopJMM
 *------------------------------------------------------------------------------
 * $Id$
 */
package org.rollinitiative.d20.encounter;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.TreeSet;
import java.util.concurrent.ThreadPoolExecutor;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.rollinitiative.d20.combat.Allegiance;
import org.rollinitiative.d20.combat.Combatant;
import org.rollinitiative.d20.combat.Initiative;
import org.rollinitiative.d20.combat.TurnProcessor;
import org.rollinitiative.d20.dice.Dice;
import org.rollinitiative.d20.entity.Actor;

/**
 * The Encounter class manages combatants participating in an encounter. Not even remotely
 * thread-safe at the moment.
 * 
 * Basic Usage:
 * <ol>
 * <li>Instantiate the encounter and add all the participants via joinEncounter</li>
 * <li>Begin the encounter by invoking initiate()</li>
 * </ol>
 * 
 * @author bebopJMM
 * 
 */
public class Encounter
{
    static final Log LOG_ = LogFactory.getLog(Encounter.class);

    /**
     * Optional identifying name for this encounter.
     */
    String name_ = null;

    /**
     * Counter of the current round of this encounter.
     */
    int round_ = 0;

    /**
     * Flag if the encounter is currently active and in progress
     */
    boolean active_ = false;

    /**
   * @deprecated
   */
//    Hashtable<Actor, Combatant> combatants_;

    Roster roster_;
    
    /**
   * 
   */
    Dice dice_ = Dice.getInstance();

    TurnProcessor turnEngine_;

    private ThreadPoolExecutor threadPool_;


    /**
     * @param name
     */
    public Encounter(String name)
    {
        super();
        this.name_ = name;
        this.roster_ = new Roster();
//        this.combatants_ = new Hashtable<Actor, Combatant>();
    }


    /**
     * Adds the specified actor to the encounter under the designated allegiance
     * 
     * @param actor
     * @param allegiance
     * @since incr-0.1
     */
    public void joinEncounter(Actor actor, Allegiance allegiance)
    {
        LOG_.info("Adding Actor to encounter, name = " + actor.getName());

        // Verify the Actor is not already part of the encounter
        if (!roster_.isParticipating(actor)) {
            roster_.add(new Combatant(actor, allegiance));
//            combatants_.put(actor, new Combatant(actor, allegiance));
            LOG_.debug("Total combatants = " + roster_.getNumberCombatants());
        }
        else {
            LOG_.warn("Actor already part of encounter. Combatant NOT added");
        }
    }


    public Combatant getCombatant(Actor actor)
    {
        return roster_.getCombatant(actor);
//        return combatants_.get(actor);
    }


    /**
     * Removes the specified actor from the encounter.
     * 
     * @param actor
     * @since incr-0.1
     */
    public void leaveEncounter(Actor actor)
    {
        LOG_.info("Removing Actor from encounter, name = " + actor.getName());
        Combatant combatant = roster_.getCombatant(actor);
        roster_.remove(combatant);
//        combatants_.remove(actor);
    }


    /**
     * This method initiates the encounter. Currently presumes ALL combatants are aware.
     * 
     * @since incr-0.1
     */
    public synchronized void initiate()
    {
        // TODO Throw exception if the encounter is already active.

        active_ = true;
        round_ = 0;

        turnEngine_ = new TurnProcessor();
        turnEngine_.setEncounter(this);

        // Determine initiative order for the combat
        rollInitiative();

        // TODO: Add support for surprise and flat-footed condition
    }


    /**
     * This method conducts a single round of the encounter.
     */
    public synchronized void executeRound()
    {
        if (!active_) {
            LOG_.warn("Cannot execute an Encounter that has not been initialized.");
            return;
        }
        // TODO: Launch threads for all the combatants

        // Process rounds until the round is complete
        round_++;
        LOG_.info("Beginnning combat sequence for round: " + round_);
        Iterator<Combatant> combatants;
        Combatant nextCombatant = null;

        LOG_.info("Round = " + round_);
        combatants = getInitiativeOrder();
        while (combatants.hasNext()) {
            nextCombatant = combatants.next();
            LOG_.info("Next up is " + nextCombatant.getActor().getName());
            // retrieve and execute actions selected by the combatant
            turnEngine_.process(nextCombatant.getActionSet());
        }

    }


    /**
     * This method rolls initiative values for all combatants. Ties are resolved using initiative
     * modifier as the first discriminator, followed by additional rolls until the order is
     * resolved.
     * 
     * @since incr-0.1
     */
    private void rollInitiative()
    {
        Initiative initiative;

        Hashtable<Integer, ArrayList<Combatant>> tieTable = new Hashtable<Integer, ArrayList<Combatant>>();
        ArrayList<Combatant> tiedCombatants = null;
        TreeSet<Integer> tieSets = new TreeSet<Integer>();
        Integer rollInt;

        // Re-roll initiative for all combatants
        Combatant[] combatant = new Combatant[roster_.getNumberCombatants()];
        combatant = roster_.getCombatants().toArray(combatant);
        LOG_.debug("Total combatants = " + combatant.length);
        for (int i = 0; i < combatant.length; i++) {
            initiative = combatant[i].rollInitiative();
            LOG_.debug("Combatant, " + combatant[i].getActor().getName() + " has initiative = "
                    + initiative.getValue());
            rollInt = Integer.valueOf(initiative.getValue());
            if (tieTable.containsKey(rollInt)) {
                tiedCombatants = tieTable.get(rollInt);
                tieSets.add(rollInt);
                LOG_.debug("Possible new tie for initiative = " + initiative.getValue());
            }
            else {
                tiedCombatants = new ArrayList<Combatant>();
            }
            tiedCombatants.add(combatant[i]);
            tieTable.put(rollInt, tiedCombatants);
        }

        // Resolve any ties
        LOG_.info("Total sets of ties = " + tieSets.size());
        Iterator<Integer> tieSetIter = tieSets.iterator();
        Integer initTie;
        Combatant nextCombatant;
        ArrayList<Combatant> resolveList = new ArrayList<Combatant>();
        while (tieSetIter.hasNext()) {
            initTie = tieSetIter.next();
            tiedCombatants = tieTable.get(initTie);
            LOG_.debug("Num combatants tied at initiative, " + initTie + ", = "
                    + tiedCombatants.size());
            // After sorting, the combatants are ordered by DECREASING initiative
            // modifier (highest to lowest)
            Collections.sort(tiedCombatants);
            nextCombatant = tiedCombatants.get(0);
            resolveList.add(nextCombatant);
            for (int i = 1; i < tiedCombatants.size(); i++) {
                if (nextCombatant.compareTo(tiedCombatants.get(i)) == 0) {
                    LOG_.debug(nextCombatant.getActor().getName()
                            + " has identical initModifier as "
                            + tiedCombatants.get(i).getActor().getName());
                    resolveList.add(tiedCombatants.get(i));
                }
                else { // end of matching modifiers
                    LOG_.debug(nextCombatant.getActor().getName()
                            + " has different initModifier as "
                            + tiedCombatants.get(i).getActor().getName());
                    if (resolveList.size() > 1) {
                        resolveTies(resolveList);
                    }
                    // prepare for next set.
                    resolveList.clear();
                    nextCombatant = tiedCombatants.get(i);
                    resolveList.add(nextCombatant);

                }
            }
            if (resolveList.size() > 1) {
                resolveTies(resolveList);
                resolveList.clear();
            }

        }

    }


    /**
     * @return
     */
    public Iterator<Combatant> getInitiativeOrder()
    {
        TreeSet<Combatant> ts = new TreeSet<Combatant>(roster_.getCombatants());
        LOG_.debug("TreeSet size = " + ts.size());
        return ts.iterator();
    }


    /**
     * @return the name_
     */
    public String getName()
    {
        return name_;
    }


    /**
     * @param name_ the name_ to set
     */
    public void setName(String name_)
    {
        this.name_ = name_;
    }


    /**
     * @return the active_
     */
    public boolean isActive()
    {
        return active_;
    }


    /**
     * @return the round_
     */
    public int getRound()
    {
        return round_;
    }


    private void resolveTies(ArrayList<Combatant> tiedCombatants)
    {
        LOG_.info("Resolving initiative tie between combatants, num = " + tiedCombatants.size());
        TreeSet<Combatant> ts = new TreeSet<Combatant>(tiedCombatants);
        while (ts.size() < tiedCombatants.size()) {
            Combatant[] combatant = tiedCombatants.toArray(new Combatant[tiedCombatants.size()]);

            if (LOG_.isDebugEnabled()) {
                LOG_.debug("initial order");
                for (int i = 0; i < combatant.length; i++) {
                    LOG_.debug(combatant[i].getActor().getName());
                }
            }

            for (int i = 0; i < combatant.length; i++) {
                combatant[i].rollInitiativeTiebreak();
            }
            Collections.sort(tiedCombatants);
            combatant = tiedCombatants.toArray(combatant);
            if (LOG_.isDebugEnabled()) {
                LOG_.debug("tie-break order");
                for (int i = 0; i < combatant.length; i++) {
                    LOG_.debug(combatant[i].getActor().getName() + " with tieRoll = "
                            + combatant[i].getInitiative().getTieRoll());
                }
            }

            ts = new TreeSet<Combatant>(tiedCombatants);
        }

    }

}
